<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TensorFlow.js 线性回归演示</title>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
            color: #333;
        }
        
        .container {
            max-width: 1000px;
            width: 100%;
            background: white;
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
            padding: 30px;
            margin-top: 20px;
        }
        
        h1 {
            text-align: center;
            color: #2c3e50;
            margin-bottom: 20px;
            font-weight: 600;
        }
        
        .description {
            text-align: center;
            margin-bottom: 30px;
            color: #7f8c8d;
            line-height: 1.6;
        }
        
        .data-section {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            margin-bottom: 30px;
        }
        
        .data-card {
            flex: 1;
            min-width: 300px;
            background: #f8f9fa;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
        }
        
        .data-card h3 {
            color: #3498db;
            margin-bottom: 15px;
            border-bottom: 2px solid #eaeaea;
            padding-bottom: 10px;
        }
        
        .training-controls {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin-bottom: 30px;
        }
        
        button {
            background: #3498db;
            color: white;
            border: none;
            padding: 12px 25px;
            border-radius: 50px;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s;
            box-shadow: 0 4px 6px rgba(50, 140, 220, 0.2);
            font-weight: 600;
        }
        
        button:hover {
            background: #2980b9;
            transform: translateY(-2px);
            box-shadow: 0 6px 8px rgba(50, 140, 220, 0.3);
        }
        
        button:active {
            transform: translateY(0);
        }
        
        button:disabled {
            background: #bdc3c7;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }
        
        .visualization {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            margin-bottom: 30px;
        }
        
        .chart-container {
            flex: 1;
            min-width: 300px;
            height: 300px;
            background: #f8f9fa;
            border-radius: 10px;
            padding: 15px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
        }
        
        .output {
            background: #2c3e50;
            color: white;
            border-radius: 10px;
            padding: 20px;
            margin-top: 20px;
            min-height: 100px;
            font-family: monospace;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        
        .output h3 {
            margin-bottom: 15px;
            color: #1abc9c;
        }
        
        .progress-bar {
            width: 100%;
            height: 20px;
            background: #ecf0f1;
            border-radius: 10px;
            margin: 15px 0;
            overflow: hidden;
        }
        
        .progress {
            height: 100%;
            background: linear-gradient(90deg, #3498db, #2ecc71);
            border-radius: 10px;
            width: 0%;
            transition: width 0.3s;
        }
        
        .epoch-info {
            display: flex;
            justify-content: space-between;
            margin-bottom: 10px;
            color: #7f8c8d;
        }
        
        .prediction-result {
            background: #34495e;
            padding: 15px;
            border-radius: 8px;
            margin-top: 15px;
        }
        
        .prediction-result h4 {
            color: #e74c3c;
            margin-bottom: 10px;
        }
        
        @media (max-width: 768px) {
            .data-section, .visualization {
                flex-direction: column;
            }
            
            .chart-container {
                height: 250px;
            }
        }
    </style>
</head>
<body>
    <h1>TensorFlow.js 线性回归演示</h1>
    <p class="description">此演示使用TensorFlow.js训练一个简单的线性回归模型（y = 2x + 1）</p>
    
    <div class="container">
        <div class="data-section">
            <div class="data-card">
                <h3>训练数据</h3>
                <p><strong>输入 (x):</strong> [1, 2, 3, 4, 5]</p>
                <p><strong>输出 (y):</strong> [3, 5, 7, 9, 11]</p>
                <p><strong>关系:</strong> y = 2x + 1</p>
            </div>
            
            <div class="data-card">
                <h3>模型结构</h3>
                <p><strong>层类型:</strong> 全连接层 (Dense)</p>
                <p><strong>输入维度:</strong> 1</p>
                <p><strong>输出维度:</strong> 1</p>
                <p><strong>优化器:</strong> 随机梯度下降 (SGD)</p>
                <p><strong>损失函数:</strong> 均方误差 (MSE)</p>
            </div>
        </div>
        
        <div class="training-controls">
            <button id="trainBtn" onclick="trainModel()">开始训练模型</button>
            <div class="epoch-info">
                <span>迭代次数: <span id="epochCount">0</span>/100</span>
                <span>损失值: <span id="lossValue">-</span></span>
            </div>
            <div class="progress-bar">
                <div class="progress" id="progress"></div>
            </div>
        </div>
        
        <div class="visualization">
            <div class="chart-container">
                <h3>训练损失变化</h3>
                <canvas id="lossChart"></canvas>
            </div>
            
            <div class="chart-container">
                <h3>拟合结果</h3>
                <canvas id="fitChart"></canvas>
            </div>
        </div>
        
        <div class="output">
            <h3>训练输出</h3>
            <div id="output">准备训练...</div>
            
            <div class="prediction-result">
                <h4>预测结果</h4>
                <p id="prediction">模型尚未训练，无法进行预测</p>
            </div>
        </div>
    </div>

    <script>
        // 模型和训练变量
        let model;
        const xs = tf.tensor1d([1, 2, 3, 4, 5]);
        const ys = tf.tensor1d([3, 5, 7, 9, 11]);
        let lossHistory = [];
        
        // 初始化模型
        function createModel() {
            const model = tf.sequential();
            model.add(tf.layers.dense({ units: 1, inputShape: [1] }));
            return model;
        }
        
        // 准备训练图表
        function setupCharts() {
            // 这里只是示意，实际应用中可以使用Chart.js等库
            const lossCtx = document.getElementById('lossChart').getContext('2d');
            const fitCtx = document.getElementById('fitChart').getContext('2d');
            
            // 绘制简单的图表占位符
            lossCtx.fillStyle = '#f0f0f0';
            lossCtx.fillRect(0, 0, lossCtx.canvas.width, lossCtx.canvas.height);
            lossCtx.fillStyle = '#3498db';
            lossCtx.textAlign = 'center';
            lossCtx.fillText('训练损失图表', lossCtx.canvas.width/2, lossCtx.canvas.height/2);
            
            fitCtx.fillStyle = '#f0f0f0';
            fitCtx.fillRect(0, 0, fitCtx.canvas.width, fitCtx.canvas.height);
            fitCtx.fillStyle = '#3498db';
            fitCtx.textAlign = 'center';
            fitCtx.fillText('拟合结果图表', fitCtx.canvas.width/2, fitCtx.canvas.height/2);
        }
        
        // 训练模型
        async function trainModel() {
            // 禁用按钮，防止重复点击
            document.getElementById('trainBtn').disabled = true;
            document.getElementById('output').innerHTML = '训练中...';
            
            // 创建模型
            model = createModel();
            model.compile({
                optimizer: 'sgd',
                loss: 'meanSquaredError'
            });
            
            // 执行训练
            await model.fit(xs, ys, {
                epochs: 100,
                callbacks: {
                    onEpochBegin: async (epoch, logs) => {
                        // 更新epoch计数
                        document.getElementById('epochCount').textContent = epoch + 1;
                    },
                    onEpochEnd: async (epoch, logs) => {
                        // 更新损失值
                        document.getElementById('lossValue').textContent = logs.loss.toFixed(4);
                        
                        // 更新进度条
                        document.getElementById('progress').style.width = `${(epoch + 1)}%`;
                        
                        // 保存损失历史
                        lossHistory.push(logs.loss);
                        
                        // 更新输出
                        document.getElementById('output').innerHTML = 
                            `迭代次数: ${epoch+1}, 当前误差: ${logs.loss.toFixed(4)}`;
                        
                        // 为了不阻塞UI，每训练一步等待一下
                        await tf.nextFrame();
                    }
                }
            });
            
            // 训练完成
            document.getElementById('output').innerHTML += '<br>训练完成！';
            
            // 进行预测
            const testX = 10;
            const testXTensor = tf.tensor1d([testX]);
            const prediction = model.predict(testXTensor);
            const predictedValue = prediction.dataSync()[0];
            
            // 显示预测结果
            document.getElementById('prediction').innerHTML = 
                `对于输入值 x = ${testX}, 预测结果 y = ${predictedValue.toFixed(2)}<br>` +
                `真实值应为: ${2*testX + 1} (根据 y = 2x + 1)<br>` +
                `误差: ${Math.abs(predictedValue - (2*testX + 1)).toFixed(2)}`;
            
            // 启用按钮
            document.getElementById('trainBtn').disabled = false;
            
            // 更新图表
            updateCharts();
        }
        
        // 更新图表
        function updateCharts() {
            // 这里只是示意，实际应用中可以使用Chart.js等库更新图表
            const lossCtx = document.getElementById('lossChart').getContext('2d');
            const fitCtx = document.getElementById('fitChart').getContext('2d');
            
            // 清空画布
            lossCtx.clearRect(0, 0, lossCtx.canvas.width, lossCtx.canvas.height);
            fitCtx.clearRect(0, 0, fitCtx.canvas.width, fitCtx.canvas.height);
            
            // 绘制损失图表
            lossCtx.fillStyle = '#f0f0f0';
            lossCtx.fillRect(0, 0, lossCtx.canvas.width, lossCtx.canvas.height);
            
            if (lossHistory.length > 0) {
                lossCtx.beginPath();
                lossCtx.moveTo(0, lossCtx.canvas.height);
                
                const maxLoss = Math.max(...lossHistory);
                const minLoss = Math.min(...lossHistory);
                const lossRange = maxLoss - minLoss || 1; // 避免除以零
                
                for (let i = 0; i < lossHistory.length; i++) {
                    const x = (i / (lossHistory.length - 1)) * lossCtx.canvas.width;
                    const y = lossCtx.canvas.height - ((lossHistory[i] - minLoss) / lossRange) * lossCtx.canvas.height;
                    lossCtx.lineTo(x, y);
                }
                
                lossCtx.strokeStyle = '#e74c3c';
                lossCtx.lineWidth = 2;
                lossCtx.stroke();
                
                lossCtx.fillStyle = '#3498db';
                lossCtx.textAlign = 'center';
                lossCtx.fillText('训练损失变化', lossCtx.canvas.width/2, 20);
            }
            
            // 绘制拟合图表
            fitCtx.fillStyle = '#f0f0f0';
            fitCtx.fillRect(0, 0, fitCtx.canvas.width, fitCtx.canvas.height);
            
            // 绘制原始数据点
            const originalX = [1, 2, 3, 4, 5];
            const originalY = [3, 5, 7, 9, 11];
            
            // 计算坐标范围
            const maxX = Math.max(...originalX, 10);
            const maxY = Math.max(...originalY, 21);
            
            // 绘制坐标轴
            fitCtx.beginPath();
            fitCtx.moveTo(30, fitCtx.canvas.height - 30);
            fitCtx.lineTo(fitCtx.canvas.width - 10, fitCtx.canvas.height - 30);
            fitCtx.moveTo(30, 10);
            fitCtx.lineTo(30, fitCtx.canvas.height - 30);
            fitCtx.strokeStyle = '#7f8c8d';
            fitCtx.stroke();
            
            // 绘制数据点
            for (let i = 0; i < originalX.length; i++) {
                const x = 30 + (originalX[i] / maxX) * (fitCtx.canvas.width - 40);
                const y = fitCtx.canvas.height - 30 - (originalY[i] / maxY) * (fitCtx.canvas.height - 40);
                
                fitCtx.beginPath();
                fitCtx.arc(x, y, 5, 0, Math.PI * 2);
                fitCtx.fillStyle = '#e74c3c';
                fitCtx.fill();
            }
            
            // 绘制预测点
            if (model) {
                const testX = 10;
                const testXTensor = tf.tensor1d([testX]);
                const prediction = model.predict(testXTensor);
                const predictedValue = prediction.dataSync()[0];
                
                const x = 30 + (testX / maxX) * (fitCtx.canvas.width - 40);
                const y = fitCtx.canvas.height - 30 - (predictedValue / maxY) * (fitCtx.canvas.height - 40);
                
                fitCtx.beginPath();
                fitCtx.arc(x, y, 5, 0, Math.PI * 2);
                fitCtx.fillStyle = '#3498db';
                fitCtx.fill();
                
                // 绘制拟合线
                fitCtx.beginPath();
                fitCtx.moveTo(30, fitCtx.canvas.height - 30);
                
                const x1 = 30 + (1 / maxX) * (fitCtx.canvas.width - 40);
                const y1 = fitCtx.canvas.height - 30 - (model.predict(tf.tensor1d([1])).dataSync()[0] / maxY) * (fitCtx.canvas.height - 40);
                
                const x2 = 30 + (testX / maxX) * (fitCtx.canvas.width - 40);
                const y2 = fitCtx.canvas.height - 30 - (predictedValue / maxY) * (fitCtx.canvas.height - 40);
                
                fitCtx.moveTo(x1, y1);
                fitCtx.lineTo(x2, y2);
                fitCtx.strokeStyle = '#2ecc71';
                fitCtx.lineWidth = 2;
                fitCtx.stroke();
            }
            
            fitCtx.fillStyle = '#3498db';
            fitCtx.textAlign = 'center';
            fitCtx.fillText('拟合结果', fitCtx.canvas.width/2, 20);
        }
        
        // 初始化页面
        window.onload = function() {
            setupCharts();
        };
    </script>
</body>
</html>